import java.io.File;
import java.util.Enumeration;
import java.util.Stack;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;

/**
 * QueryProcessFacade
 * 
 * This class takes a Query object and tokenizes it accoring to the following
 * grammar:
 * 
 * query :=: term_rec term_rec :=: term' term_rec | NULL term' :=: atom '|'
 * term' | atom atom :=: WORD | '(' query ')' WORD :=: a series of one or more
 * non-break characters
 * 
 * @author Zachary M. Allen (doResults()), Jack Keyser (all other methods)
 */
class QueryProcessFacade {

	/**
	 * Constant represents left parenthesis
	 */
	private static final char LPARAN = '(';

	/**
	 * Constant represents right parenthesis
	 */
	private static final char RPARAN = ')';

	/**
	 * Constant represents OR operator
	 */
	private static final char OR = '|';

	/**
	 * Constant represents space character
	 */
	private static final char SPACE = ' ';

	/**
	 * Constants represents error message when a stop word is queried
	 */
	private static final String CANNOT_QUERY_STOP_WORD = "Cannot query stop word";

	/**
	 * The user's most recently queried query
	 */
	private Query mostRecentQuery;

	/**
	 * The result set matching the most recently processed query
	 */
	private ResultSet mostRecentResult;

	/**
	 * The root node of the tokenized and processed query
	 */
	private DefaultMutableTreeNode rootNode;

	/**
	 * Only one instance of this class may exist at a time; this variable holds
	 * that instance
	 */
	private static QueryProcessFacade singleton;

	/**
	 * Stack that holds the tokens parsed from a query
	 */
	private Stack tokenHistory;

	/**
	 * The offset into the current token
	 */
	private int offset;

	/**
	 * The base of the current token
	 */
	private int base;

	/**
	 * The FileIndex object that is queried
	 */
	private FileIndex fileIndex;

	/**
	 * The reader that read the index file into the FileIndex
	 */
	private CorpusIndexReader reader;

	/**
	 * Returns an instance of QueryProcessFacade
	 * 
	 * @return An instance of QueryProcessFacade
	 */
	static synchronized QueryProcessFacade Instance() {
		if (null == singleton) {
			singleton = new QueryProcessFacade();
		}
		return singleton;
	}

	/**
	 * Getter returns value of rootNode
	 * 
	 * @return Current value of rootNode
	 */
	public DefaultMutableTreeNode getSyntaxTree() {
		return rootNode;
	}

	/**
	 * This method takes a Query and ensures that it is tokenized and parsed
	 * properly. Upon return, mostRecentResult is set to the result set that
	 * matches the given query.
	 * 
	 * @param q
	 *            Query to process
	 * @param fi
	 *            FileIndex that was read from disk
	 * @param r
	 *            CorpusIndexReader that read index
	 * @throws QUIRKException
	 */
	void processQuery(Query q, FileIndex fi, CorpusIndexReader r)
			throws QUIRKException {
		fileIndex = fi;
		reader = r;
		mostRecentQuery = q;
		tokenHistory = new Stack();
		rootNode = new DefaultMutableTreeNode("query");
		query(rootNode);
		doResults();
	}

	/**
	 * Processes the newly parsed query. If a stop word was included in the
	 * query, this method handles that.
	 * 
	 * @throws QUIRKException
	 */
	private void doResults() throws QUIRKException {
		try {
			mostRecentResult = process(rootNode);
		} catch (NullPointerException e) {
			e.printStackTrace();
			System.out.println(CANNOT_QUERY_STOP_WORD);
			mostRecentResult = new ResultSet();
		}
		if (mostRecentResult == null) {
			System.out.println(CANNOT_QUERY_STOP_WORD);
			mostRecentResult = new ResultSet();
		}
	}

	/**
	 * Processes a parsed query tree node recursively, handling each token type
	 * appropriately.
	 * 
	 * @param node
	 *            A Node with child terms to form a result set for.
	 * @return A ResultSet that matches the proper operations among all children
	 *         of a given node.
	 * @throws QUIRKException
	 * @throws NullPointerException
	 */
	private ResultSet process(MutableTreeNode node) throws QUIRKException,
			NullPointerException {

		String name = node.toString();
		ResultSet retval = new ResultSet();
		StopWordList swl = new StopWordList(fileIndex.getVariableBreaks());
		if (!swl.parseFile(fileIndex.getStopWordFile())) {
			throw new QUIRKException(
					QUIRKException.CANNOT_PROCESS_STOP_WORD_FILE);
		}

		if (name.equals("query") && node.getChildCount() == 1) {
			retval = process((MutableTreeNode) node.getChildAt(0));
		} else if (name.equals("term_rec") && node.getChildCount() == 1) {
			retval = process((MutableTreeNode) node.getChildAt(0));
		} else if (name.equals("term_rec") && node.getChildCount() > 1) {
			retval = process((MutableTreeNode) node.getChildAt(0));
			for (Enumeration children = node.children(); children
					.hasMoreElements();) {
				MutableTreeNode child = (MutableTreeNode) children
						.nextElement();
				retval.intersect(process(child));
			}
		} else if (name.equals("term_prime") && node.getChildCount() == 1) {
			retval = process((MutableTreeNode) node.getChildAt(0));
		} else if (name.equals("term_prime") && node.getChildCount() == 3) {
			retval = process((MutableTreeNode) node.getChildAt(0));
			retval.union(process((MutableTreeNode) node.getChildAt(2)));
		} else if (name.equals("atom") && node.getChildCount() == 1) {
			retval = process((MutableTreeNode) node.getChildAt(0));
		} else if (name.equals("atom") && node.getChildCount() == 3) {
			retval = process((MutableTreeNode) node.getChildAt(1));
		} else if (name.startsWith("WORD: ")) {
			String word = name.substring("WORD: ".length());
			if (swl.isStop(word)) {
				return null;
			}
			for (int i = 0; i < fileIndex.getFileIndexInstances().size(); i++) {
				FileIndexInstance fi = (FileIndexInstance) fileIndex
						.getFileIndexInstances().get(i);
				for (int j = 0; j < fi.lineSize; j++) {
					if (reader.isWordOnLine(word, i, j)) {
						File f = new File(fi.filename);
						if (f.lastModified() > fileIndex.getTime()
								.getTimeInMillis()) {
							throw new QUIRKException(
									QUIRKException.OUT_OF_DATE_INDEX);
						}
						retval.add(word, fi.filename, j, fileIndex
								.getVariableBreaks());
					}
				}
			}
		}
		return retval;
	}

	/**
	 * Based on the current position in a query, returns the next token in the
	 * raw query. Saves the current offet and base between calls to ensure
	 * proper tokenizing
	 * 
	 * @return Next token in raw query
	 */
	private String nextToken() {

		String token = "";

		for (offset = 0; base + offset < mostRecentQuery.toString().length(); offset++) {

			if (base + offset + 1 < mostRecentQuery.toString().toString()
					.length()
					&& (mostRecentQuery.toString().charAt(base + offset + 1) == LPARAN
							|| mostRecentQuery.toString().charAt(
									base + offset + 1) == RPARAN
							|| mostRecentQuery.toString().charAt(
									base + offset + 1) == OR || mostRecentQuery
							.toString().charAt(base + offset + 1) == SPACE)) {
				break;
			} else if (base + offset < mostRecentQuery.toString().toString()
					.length()
					&& (mostRecentQuery.toString().toString().charAt(
							base + offset) == LPARAN
							|| mostRecentQuery.toString().toString().charAt(
									base + offset) == RPARAN || mostRecentQuery
							.toString().toString().charAt(base + offset) == OR)) {
				break;
			}

		}

		if (base + offset == mostRecentQuery.toString().length()) {
			offset--;
		}

		if (base + offset < mostRecentQuery.toString().length()) {
			token = mostRecentQuery.toString().substring(base,
					base + offset + 1);
			base = base + offset + 1;
		}

		return token;

	}

	/**
	 * Determines if root node represents a valid tokenized query.
	 * 
	 * @param node
	 *            Root node in tokenized query
	 * @return True if node is a valid query, false otherwise
	 */
	private boolean query(DefaultMutableTreeNode node) {
		DefaultMutableTreeNode termRecNode = new DefaultMutableTreeNode(
				"term_rec");
		boolean isGood = false;
		if (isGood = term_rec(termRecNode)) {
			node.add(termRecNode);
		}
		return isGood;
	}

	/**
	 * Determines if a node represents a valid term_rec
	 * 
	 * @param node
	 *            N in tokenized query
	 * @return True if node is a valid term_rec, false otherwise
	 */
	private boolean term_rec(DefaultMutableTreeNode node) {
		DefaultMutableTreeNode term_prime = new DefaultMutableTreeNode(
				"term_prime");
		DefaultMutableTreeNode term_rec = new DefaultMutableTreeNode("term_rec");

		if (term_prime(term_prime)) {
			node.add(term_prime);

			if (term_rec(term_rec)) {
				node.add(term_rec);
			}

			return true;
		}

		return false;
	}

	/**
	 * Determines if a node represents a valid term_prime
	 * 
	 * @param node
	 *            N in tokenized query
	 * @return True if node is a valid term_prime, false otherwise
	 */
	private boolean term_prime(DefaultMutableTreeNode node) {
		DefaultMutableTreeNode ORnode = new DefaultMutableTreeNode("OR");
		DefaultMutableTreeNode atomNode = new DefaultMutableTreeNode("atom");
		DefaultMutableTreeNode termPrimeNode = new DefaultMutableTreeNode(
				"term_prime");

		if (atom(atomNode)) {
			node.add(atomNode);

			if (tokenHistory.empty()) {
				tokenHistory.push(nextToken());
			}

			String topToken = (String) tokenHistory.peek();

			if (topToken.equals("" + OR)) {
				node.add(ORnode);
				tokenHistory.pop();

				if (term_prime(termPrimeNode)) {
					node.add(termPrimeNode);
				} else {
					return false;
				}
			}

			return true;
		} else {
			return false;
		}

	}

	/**
	 * Determines if a node represents a valid atom
	 * 
	 * @param node
	 *            N in tokenized query
	 * @return True if node is a valid atom, false otherwise
	 */
	private boolean atom(DefaultMutableTreeNode node) {
		if (tokenHistory.empty())
			tokenHistory.push(nextToken());

		String val = (String) tokenHistory.peek();

		DefaultMutableTreeNode queryNode = new DefaultMutableTreeNode("query");

		if (!val.equals("" + LPARAN) && !val.equals("" + RPARAN)
				&& !val.equals("" + OR) && !val.equals("") && !val.equals(" ")) {
			node.add(new DefaultMutableTreeNode("WORD: " + val.trim()));
			tokenHistory.pop();
			return true;
		} else if (val.equals("" + LPARAN)) {
			tokenHistory.pop();
			node.add(new DefaultMutableTreeNode("" + LPARAN));

			if (!query(queryNode)) {
				return false;
			} else {
				node.add(queryNode);
			}

			if (tokenHistory.empty()) {
				tokenHistory.push(nextToken());
			}

			val = (String) tokenHistory.peek();

			if (val.equals("" + RPARAN)) {
				tokenHistory.pop();
				node.add(new DefaultMutableTreeNode("" + RPARAN));
				return true;
			}

			return false;
			
		} else {
			return false;
		}

	}

	/**
	 * Getter returns value of mostRecentResult
	 * 
	 * @return Current value of mostRecentResult
	 */
	public ResultSet getMostRecentResult() {
		return mostRecentResult;
	}

	/**
	 * Getter returns value of mostRecentQuery
	 * 
	 * @return Current value of mostRecentQuery
	 */
	public Query getMostRecentQuery() {
		return mostRecentQuery;
	}

	/**
	 * Resets the QueryProcessFacade by killing the instance, forcing a new one
	 * to be created
	 * 
	 */
	public void reset() {
		singleton = null;
	}

}

/**
 * Query.java
 * 
 * This class represents an Query made by a user. The QueryProcessFacade parses
 * and tokenizes the query, recursively creating a final ResultSet object that
 * represents the results of the query.
 * 
 * @author Zachary M. Allen
 */
public class Query {

	/**
	 * The raw query String
	 */
	private String query;

	/**
	 * Constructor
	 * 
	 * @param q
	 *            New raw query String
	 */
	public Query(String q) {
		query = q;
	}

	/**
	 * Returns String representation of this Query
	 */
	public String toString() {
		return query;
	}

}